/*ESP8266 Horizontal Shooter Game on 8x32 Matrix WS2812b by mircemk, May 2025 */ #include // Matrix dimensions #define MATRIX_WIDTH 32 #define MATRIX_HEIGHT 8 #define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT) // Pin definitions #define LED_PIN D6 #define BTN_UP D2 #define BTN_DOWN D3 #define BTN_FIRE D4 #define BUZZER_PIN D8 // Game parameters #define PLAYER_COLOR CRGB::Green #define MISSILE_COLOR CRGB::Yellow #define ENEMY_COLOR CRGB::Red #define ENEMY_WEAPON_COLOR CRGB::Magenta #define ENEMY_MISSILE_COLOR CRGB::Magenta #define PLAYER_SPEED 1 #define MISSILE_SPEED 2 #define ENEMY_SPEED 0.5f #define ENEMY_FIRE_RATE 0.02 #define SCROLL_SPEED 80 #define START_TEXT "PRESS FIRE TO START" #define GAMEOVER_TEXT "GAME OVER" #define SCORE_TEXT "SCORE - " // Sound definitions #define SOUND_SHOOT_FREQ 2000 #define SOUND_SHOOT_DURATION 40 #define SOUND_ENEMY_HIT_FREQ1 800 #define SOUND_ENEMY_HIT_FREQ2 1200 #define SOUND_ENEMY_HIT_DURATION 20 #define SOUND_GAMEOVER_FREQ1 400 #define SOUND_GAMEOVER_FREQ2 300 #define SOUND_GAMEOVER_DURATION 100 #define SOUND_START_FREQ 1000 #define SOUND_START_DURATION 150 #define SOUND_SCORE_FREQ 1500 #define SOUND_SCORE_DURATION 50 #define SQUARE_WAVE_DUTY_CYCLE 50 // Percentage of time the pin is HIGH // Sound management unsigned long soundEndTime = 0; bool soundActive = false; void playTone(int frequency, int duration) { unsigned long period = 1000000L / frequency; // Period in microseconds unsigned long halfPeriod = period / 2; unsigned long startTime = micros(); while (micros() - startTime < duration * 1000L) { digitalWrite(BUZZER_PIN, HIGH); delayMicroseconds(halfPeriod); digitalWrite(BUZZER_PIN, LOW); delayMicroseconds(halfPeriod); } noTone(BUZZER_PIN); // Ensure the buzzer is off after the tone } void updateSound() { if (soundActive && millis() > soundEndTime) { noTone(BUZZER_PIN); soundActive = false; } } void playEnemyDestroyedSound() { playTone(SOUND_ENEMY_HIT_FREQ1, SOUND_ENEMY_HIT_DURATION); delay(60); playTone(SOUND_ENEMY_HIT_FREQ2, SOUND_ENEMY_HIT_DURATION); } void playGameOverSound() { playTone(SOUND_GAMEOVER_FREQ1, SOUND_GAMEOVER_DURATION); delay(350); playTone(SOUND_GAMEOVER_FREQ2, SOUND_GAMEOVER_DURATION); } // Game state variables bool upButtonPressed = false; bool downButtonPressed = false; unsigned long lastMoveTime = 0; #define MOVE_COOLDOWN 100 CRGB leds[NUM_LEDS]; enum GameState { TITLE_SCREEN, PLAYING, GAME_OVER, SCORE_DISPLAY }; GameState gameState = TITLE_SCREEN; unsigned long gameOverStartTime = 0; bool waitingForFireButton = false; // Game objects struct Player { int x = 0; int y = MATRIX_HEIGHT / 2; bool moveRequested = false; } player; struct Missile { float xPos; int x; int y; bool active = false; }; #define MAX_MISSILES 3 Missile playerMissiles[MAX_MISSILES]; Missile enemyMissiles[5]; struct Enemy { float xPos; int x; int y; bool active = false; bool hasWeapon = true; }; #define MAX_ENEMIES 3 Enemy enemies[MAX_ENEMIES]; int score = 0; int lives = 3; unsigned long lastEnemySpawn = 0; #define ENEMY_SPAWN_RATE 1500 bool fireButtonPressed = false; unsigned long lastFireTime = 0; #define FIRE_COOLDOWN 300 // Font 5x4 const uint8_t font5x4[44][4] = { {31,20,20,31}, {31,21,21,10}, {14,17,17,10}, {31,17,17,14}, {31,21,21,17}, {31,20,20,16}, {14,17,21,14}, {31,4,4,31}, {17,31,17,17}, {2,1,1,30}, {31,4,10,17}, {31,1,1,1}, {31,12,12,31}, {31,12,3,31}, {14,17,17,14}, {31,20,20,8}, {14,17,19,14}, {31,20,22,9}, {8,21,21,2}, {16,16,31,16}, {30,1,1,30}, {28,3,3,28}, {31,3,12,31}, {27,4,4,27}, {24,4,3,28}, {19,21,25,17}, {0,0,0,0}, {14,17,17,14}, {0,17,31,1}, {19,21,21,9}, {17,21,21,14}, {28,4,4,31}, {30,21,21,18}, {14,21,21,2}, {16,19,20,24}, {10,21,21,10}, {8,21,21,14}, {0,4,0,0}, {0,0,0,0} }; int getCharIndex(char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= '0' && c <= '9') return c - '0' + 26; // Correct mapping for digits if (c == '-') return 36; if (c == ' ') return 37; return 37; } int XY(int x, int y) { int flippedX = (MATRIX_WIDTH - 1) - x; int flippedY = (MATRIX_HEIGHT - 1) - y; if (flippedX % 2 == 0) { return (flippedX * MATRIX_HEIGHT) + flippedY; } else { return (flippedX * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - flippedY); } } int XY_text(int x, int y) { x = (MATRIX_WIDTH - 1) - x; y = (MATRIX_HEIGHT - 1) - y; if (x % 2 == 0) { return (x * MATRIX_HEIGHT) + y; } else { return (x * MATRIX_HEIGHT) + (MATRIX_HEIGHT - 1 - y); } } void enemyDestructionAnimation(int enemyX, int enemyY) { // Light up the top and bottom positions around the enemy's red LED // Adjust the positions if necessary to match your LED layout leds[XY(enemyX, enemyY - 1)] = CRGB::Orange; leds[XY(enemyX, enemyY + 1)] = CRGB::Orange; FastLED.show(); // Keep the animation for 100 milliseconds before clearing delay(100); // Clear the animated LEDs (set to black) leds[XY(enemyX, enemyY - 1)] = CRGB::Black; leds[XY(enemyX, enemyY + 1)] = CRGB::Black; FastLED.show(); } void drawChar(int x, int y, char c, CRGB color) { int charIndex; if (c >= '0' && c <= '9') { charIndex = getCharIndex(c + 1); // Try adding 1 to the character value } else { charIndex = getCharIndex(c); } for (int col = 0; col < 4; col++) { if (x + col >= 0 && x + col < MATRIX_WIDTH) { uint8_t pattern = font5x4[charIndex][col]; for (int row = 0; row < 5; row++) { if (pattern & (1 << (4 - row))) { if (y + row >= 0 && y + row < MATRIX_HEIGHT) { leds[XY_text(x + col, y + row)] = color; } } } } } } void scrollText(const char* text, CRGB color) { static int scrollX = MATRIX_WIDTH; static unsigned long lastScroll = 0; if (millis() - lastScroll > SCROLL_SPEED) { FastLED.clear(); int textLen = strlen(text); for (int i = 0; i < textLen; i++) { drawChar(scrollX + (i * 5), 1, text[i], color); } FastLED.show(); scrollX--; if (scrollX < -(textLen * 5)) { scrollX = MATRIX_WIDTH; } lastScroll = millis(); } } void firePlayerMissile() { for (int i = 0; i < MAX_MISSILES; i++) { if (!playerMissiles[i].active) { playerMissiles[i].xPos = player.x + 1; playerMissiles[i].x = (int)playerMissiles[i].xPos; playerMissiles[i].y = player.y; playerMissiles[i].active = true; playTone(SOUND_SHOOT_FREQ, SOUND_SHOOT_DURATION); break; } } } void handleInput() { if (digitalRead(BTN_UP) == LOW) { if (!upButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) { player.y = max(0, player.y - 1); upButtonPressed = true; lastMoveTime = millis(); } } else { upButtonPressed = false; } if (digitalRead(BTN_DOWN) == LOW) { if (!downButtonPressed && millis() - lastMoveTime > MOVE_COOLDOWN) { player.y = min(MATRIX_HEIGHT-1, player.y + 1); downButtonPressed = true; lastMoveTime = millis(); } } else { downButtonPressed = false; } if (digitalRead(BTN_FIRE) == LOW) { if (!fireButtonPressed && millis() - lastFireTime > FIRE_COOLDOWN) { firePlayerMissile(); fireButtonPressed = true; lastFireTime = millis(); } } else { fireButtonPressed = false; } } void updateGame() { // Update player missiles for (int i = 0; i < MAX_MISSILES; i++) { if (playerMissiles[i].active) { playerMissiles[i].xPos += MISSILE_SPEED; playerMissiles[i].x = (int)playerMissiles[i].xPos; if (playerMissiles[i].x >= MATRIX_WIDTH) { playerMissiles[i].active = false; } } } // Spawn enemies if (millis() - lastEnemySpawn > ENEMY_SPAWN_RATE) { lastEnemySpawn = millis(); for (int i = 0; i < MAX_ENEMIES; i++) { if (!enemies[i].active) { enemies[i].xPos = MATRIX_WIDTH - 1; enemies[i].x = (int)enemies[i].xPos; enemies[i].y = random(0, MATRIX_HEIGHT - 1); enemies[i].active = true; enemies[i].hasWeapon = true; break; } } } // Update enemies for (int i = 0; i < MAX_ENEMIES; i++) { if (enemies[i].active) { enemies[i].xPos -= ENEMY_SPEED; enemies[i].x = (int)enemies[i].xPos; // Enemy firing if (enemies[i].hasWeapon && random(100) < (ENEMY_FIRE_RATE * 100)) { for (int j = 0; j < 5; j++) { if (!enemyMissiles[j].active) { enemyMissiles[j].xPos = enemies[i].xPos - 1; enemyMissiles[j].x = (int)enemyMissiles[j].xPos; enemyMissiles[j].y = enemies[i].y; enemyMissiles[j].active = true; break; } } } if (enemies[i].xPos < 0) enemies[i].active = false; } } // Update enemy missiles for (int i = 0; i < 5; i++) { if (enemyMissiles[i].active) { enemyMissiles[i].xPos -= ENEMY_SPEED; enemyMissiles[i].x = (int)enemyMissiles[i].xPos; if (enemyMissiles[i].xPos < 0) enemyMissiles[i].active = false; } } checkCollisions(); } void checkCollisions() { // Player missiles vs enemies for (int m = 0; m < MAX_MISSILES; m++) { if (playerMissiles[m].active) { for (int e = 0; e < MAX_ENEMIES; e++) { if (enemies[e].active) { if ((playerMissiles[m].x == enemies[e].x && playerMissiles[m].y == enemies[e].y) || (playerMissiles[m].x == enemies[e].x - 1 && playerMissiles[m].y == enemies[e].y)) { playerMissiles[m].active = false; // Save enemy position before deactivating (for animation) int enemyX = enemies[e].x; int enemyY = enemies[e].y; enemies[e].active = false; score += 10; playEnemyDestroyedSound(); // Trigger the enemy destruction animation enemyDestructionAnimation(enemyX, enemyY); } } } } } // Enemy missiles vs player for (int i = 0; i < 5; i++) { if (enemyMissiles[i].active && enemyMissiles[i].x == player.x && enemyMissiles[i].y == player.y) { enemyMissiles[i].active = false; if (--lives <= 0) gameOver(); } } // Enemies vs player for (int i = 0; i < MAX_ENEMIES; i++) { if (enemies[i].active && enemies[i].x == player.x && enemies[i].y == player.y) { enemies[i].active = false; if (--lives <= 0) gameOver(); } } } void render() { FastLED.clear(); // Draw player leds[XY(player.x, player.y)] = PLAYER_COLOR; // Draw player missiles for (int i = 0; i < MAX_MISSILES; i++) { if (playerMissiles[i].active) { leds[XY(playerMissiles[i].x, playerMissiles[i].y)] = MISSILE_COLOR; } } // Draw enemies for (int i = 0; i < MAX_ENEMIES; i++) { if (enemies[i].active) { leds[XY(enemies[i].x, enemies[i].y)] = ENEMY_COLOR; if (enemies[i].hasWeapon && enemies[i].x > 0) { leds[XY(enemies[i].x - 1, enemies[i].y)] = ENEMY_WEAPON_COLOR; } } } // Draw enemy missiles for (int i = 0; i < 5; i++) { if (enemyMissiles[i].active) { leds[XY(enemyMissiles[i].x, enemyMissiles[i].y)] = ENEMY_MISSILE_COLOR; } } // Draw score LEDs int scoreLeds = min(score / 10, MATRIX_WIDTH); for (int i = 0; i < scoreLeds; i++) { leds[XY(MATRIX_WIDTH - 1 - i, MATRIX_HEIGHT - 1)] = CRGB::Green; } } void gameOver() { // Flash red 3 times for (int i = 0; i < 3; i++) { fill_solid(leds, NUM_LEDS, CRGB::Red); FastLED.show(); delay(300); FastLED.clear(); FastLED.show(); delay(300); } playGameOverSound(); // play sound after flashes gameState = GAME_OVER; } void loop() { static unsigned long lastFrame = millis(); if (millis() - lastFrame < 33) { delay(1); return; } lastFrame = millis(); updateSound(); // keep sound system updated! switch (gameState) { case TITLE_SCREEN: scrollText(START_TEXT, CRGB::Green); if (digitalRead(BTN_FIRE) == LOW) { playTone(SOUND_START_FREQ, SOUND_START_DURATION); gameState = PLAYING; resetGame(); delay(200); } break; case PLAYING: handleInput(); updateGame(); render(); FastLED.show(); break; case GAME_OVER: if (gameOverStartTime == 0) { gameOverStartTime = millis(); waitingForFireButton = false; } scrollText(GAMEOVER_TEXT, CRGB::Red); if (!waitingForFireButton && millis() - gameOverStartTime >= 3000) { waitingForFireButton = true; } if (waitingForFireButton && digitalRead(BTN_FIRE) == LOW) { gameState = SCORE_DISPLAY; gameOverStartTime = 0; waitingForFireButton = false; delay(200); } break; case SCORE_DISPLAY: static bool scoreInitialized = false; static unsigned long lastScrollUpdate = 0; // Control scroll update rate if (!scoreInitialized) { FastLED.clear(); scoreInitialized = true; } if (millis() - lastScrollUpdate > 75) { // Update every 75 milliseconds char scoreText[30] = "SCORE "; char scoreValue[6]; // Enough space for a 5-digit score + null terminator itoa(score, scoreValue, 10); // Convert score to string (base 10) strcat(scoreText, scoreValue); // Append score value to "SCORE " FastLED.clear(); scrollText(scoreText, CRGB::Blue); leds[XY(MATRIX_WIDTH-1, MATRIX_HEIGHT-1)] = CRGB::Black; FastLED.show(); lastScrollUpdate = millis(); } if (digitalRead(BTN_FIRE) == LOW) { scoreInitialized = false; gameState = TITLE_SCREEN; delay(200); } break; } ESP.wdtFeed(); } void setup() { Serial.begin(115200); FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(30); pinMode(BTN_UP, INPUT_PULLUP); pinMode(BTN_DOWN, INPUT_PULLUP); pinMode(BTN_FIRE, INPUT_PULLUP); pinMode(BUZZER_PIN, OUTPUT); resetGame(); } void resetGame() { FastLED.clear(); for (int i = 0; i < MAX_MISSILES; i++) playerMissiles[i].active = false; for (int i = 0; i < 5; i++) enemyMissiles[i].active = false; for (int i = 0; i < MAX_ENEMIES; i++) enemies[i].active = false; player.x = 0; player.y = MATRIX_HEIGHT / 2; score = 0; lives = 3; lastEnemySpawn = millis(); player.moveRequested = false; gameOverStartTime = 0; waitingForFireButton = false; }